Appearance
复习概念
什么是 Spring Bean
Bean 代指的就是那些被 IoC 容器所管理的对象。
我们需要告诉 IoC 容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML 文件、注解或者 Java 配置类。
Spring 中的 bean 的作用域有哪些
这个问题是面试题场景的一个问题,我们首先来看一下基本的一个回答。
Spring 中 Bean 的作用域通常有下面几种:
- singleton : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。
- prototype : 每次获取都会创建一个新的 bean 实例。也就是说,连续
getBean()两次,得到的是不同的 Bean 实例。 - request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
- session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
- application/global-session (仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。
- websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。
如何配置 bean 的作用域
xml 方式:
<bean id="..." class="..." scope="singleton"></bean>注解方式:
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Person personPrototype() {
return new Person();
}具体怎么配置的看你实际使用,这里大概讲一下使用场景,通过不同场景下的使用示例对 Spring 中 Bean 的作用域有一个基本认知。
Singleton Bean(默认作用域)
场景: 在一个在线书店应用中,有一个库存管理服务负责跟踪所有书籍的库存。这个服务需要是单例的,因为它维护着整个应用中书籍库存的统一视图。
代码逻辑
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class InventoryService {
private final Map<String, Integer> bookInventory = new ConcurrentHashMap<>();
public void addBook(String isbn, int quantity) {
bookInventory.put(isbn, bookInventory.getOrDefault(isbn, 0) + quantity);
}
public boolean checkStock(String isbn) {
return bookInventory.getOrDefault(isbn, 0) > 0;
}
// ... 其他与库存管理相关的方法 ...
}这里,InventoryService 是一个singleton作用域的Bean,它在应用的整个生命周期内只会有一个实例。
所有对书籍库存的操作都通过这个单一实例进行,确保了库存数据的一致性。
Prototype作用域
场景: 在同一在线书店应用中,当用户想要购买书籍时,每个购物车都应该是独立的。因此,每个用户的购物车应该有其自己的状态。
代码逻辑
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("prototype")
public class ShoppingCart {
private final Map<String, Integer> items = new HashMap<>();
public void addItem(String isbn) {
items.put(isbn, items.getOrDefault(isbn, 0) + 1);
}
public Map<String, Integer> getItems() {
return items;
}
// ... 购物车的其他方法 ...
}
@Service
public class ShoppingService {
@Autowired
private ApplicationContext context;
public ShoppingCart createNewShoppingCart() {
return context.getBean(ShoppingCart.class);
}
}在这个场景中,ShoppingService 使用Spring的应用上下文来为每个用户创建一个新的ShoppingCart实例。
每次调用createNewShoppingCart方法时,都会返回一个全新的购物车实例,保证了用户之间购物车的隔离。
Request作用域
场景: 在一个新闻网站应用中,你需要跟踪每个HTTP请求的访问信息,例如用户的地理位置和设备类型,以便为他们提供定制化的新闻内容。
代码逻辑
import org.springframework.web.context.annotation.RequestScope;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
@RequestScope
public class UserContext {
private final String location;
private final String deviceType;
public UserContext(HttpServletRequest request) {
// 通过请求获取用户的位置和设备类型
this.location = request.getHeader("Location");
this.deviceType = request.getHeader("Device-Type");
}
public String getLocation() {
return location;
}
public String getDeviceType() {
return deviceType;
}
}
@RestController
public class NewsController {
@Autowired
private UserContext userContext;
@GetMapping("/news")
public ResponseEntity<List<NewsItem>> getNews() {
List<NewsItem> news = newsService.getNewsForLocation(userContext.getLocation());
// 返回定制化的新闻内容
return ResponseEntity.ok(news);
}
}在这个场景中,UserContext是一个request作用域的Bean,它为每个HTTP请求提供了一个新实例,
包含了请求特定的用户上下文信息,如位置和设备类型。
Session作用域
场景: 在一个在线考试平台上,每个用户的考试过程需要被追踪。用户可能会在考试中断后返回继续考试,所以考试状态需要在会话中保持。
代码逻辑
import org.springframework.web.context.annotation.SessionScope;
import org.springframework.stereotype.Component;
@Component
@SessionScope
public class ExamSession {
private Exam currentExam;
private int currentQuestionIndex;
// 考试会话的方法,例如开始考试、回答问题等
}
@Controller
public class ExamController {
@Autowired
private ExamSession examSession;
@PostMapping("/exam/start")
public String startExam() {
examSession.startNewExam();
return "exam_started";
}
@PostMapping("/exam/answer")
public String answerQuestion(Answer answer) {
examSession.answerQuestion(answer);
return "answer_recorded";
}
}在这个场景中,ExamSession 是一个session作用域的Bean,它记录了用户的当前考试状态。每个用户会话都有自己的ExamSession实例。
Session vs Global-Session vs WebSocket
- Session:
- 作用域: 限定在一个用户的HTTP会话中。
- 典型用例: 在用户登录到网站后,你可能需要跟踪该用户的特定状态(比如购物车、偏好设置等)。这种情况下,每个用户都有自己的会话,每个会话都有自己的Bean实例。
- Global-Session:
- 作用域: 限定在Portlet环境的全局HTTP会话中。在Spring 5及之后的版本中已不再使用,因为Spring 5不再支持Portlet。
- 典型用例: 在Portlet环境中,多个Portlet可能需要共享全局会话数据。例如,用户的语言偏好可能需要跨多个Portlet保持一致。
- WebSocket:
- 作用域: 绑定到WebSocket的会话中。
- 典型用例: 当用户通过WebSocket连接到你的服务器时,你可能需要为每个WebSocket会话保存状态,例如游戏中的玩家状态或聊天应用中的用户会话
Singleton vs Prototype
Singleton vs Prototype
- Singleton:
- 描述: 在Spring IoC容器中只创建一个Bean实例。
- 典型用例: 大多数服务层和数据访问层的组件都是无状态的,可以被应用中的所有其他Bean共享。例如,数据库连接池、业务服务等。
- Prototype:
- 描述: 每次请求时,Spring IoC容器都会创建一个新的Bean实例。
- 典型用例: 比如在一个应用中,你想要为每个文件上传创建一个新的处理器(FileUploadHandler)实例,以防不同用户上传的数据相互冲突。
两种作用域的使用主要取决于Bean的状态管理需求。
Singleton用于那些不需要维护状态信息的共享组件,而Prototype适用于每次使用都需要一个新状态的场景。
Singleton vs Prototype 对比两者的生命周期区别
单例Bean的生命周期:
- 实例化: 只有一个Bean实例被创建。
- 属性填充: 容器注入依赖的属性。
- 初始化: 如果Bean实现了
InitializingBean接口或定义了自定义的初始化方法(如使用@PostConstruct注解或在XML配置中指定init-method),将会执行。 - 后处理:
BeanPostProcessors在初始化前后执行。 - 使用: Bean现在可以被应用中的其他Bean使用。
- 销毁: 当容器关闭时,如果Bean实现了
DisposableBean接口或定义了自定义的销毁方法(如使用@PreDestroy注解或在XML配置中指定destroy-method),将会执行。
多例Bean的生命周期:
- 实例化: 每次请求时都创建一个新的Bean实例。
- 属性填充: 容器注入依赖的属性。
- 初始化: 与单例Bean相同,如果有指定的初始化方法,将会执行。
- 后处理: 与单例Bean相同,
BeanPostProcessors在初始化前后执行。 - 使用: Bean被客户端获取并使用。
- 销毁: 容器不会管理多例Bean的完整生命周期;销毁由客户端负责。
区别:
- 实例化频率:
- 单例: 只在Spring IoC容器创建时实例化一次。
- 多例: 每次请求时实例化。
- 依赖注入时机:
- 单例: 依赖项在容器创建单例Bean时注入。
- 多例: 依赖项在每次创建新实例时注入。
- 生命周期管理:
- 单例: 容器负责整个生命周期,包括销毁。
- 多例: 容器启动后,不再管理Bean的生命周期;Bean的销毁不由Spring容器管理,需要用户手动管理。
- 销毁回调:
- 单例: 容器关闭时,可以调用销毁方法。
- 多例: 容器不自动调用销毁方法,必须由获取Bean的客户端代码来处理。
在实际应用中,单例Bean通常用于无状态的服务,例如业务逻辑组件和数据访问对象。
而多例Bean则用于有明确状态的操作,这些状态不能共享给其他实例或线程,例如用户的会话或独立的任务处理器。
多例Bean 的销毁操作有兴趣可以了解一下: https://springdoc.cn/spring/core.html#beans-factory-scopes-prototype
为了让Spring容器释放由 prototype scopeBean 持有的资源,可以尝试使用自定义 Bean后处理器,它持有对需要清理的Bean的引用。
参考